package felix.things.router.opc.rpc;

import com.google.common.collect.Sets;
import felix.things.router.opc.OpcUaDevice;
import felix.things.router.opc.OpcUaDeviceAware;
import felix.things.router.opc.external.MonitorWatcher;
import felix.things.router.opc.scan.OpcUaNode;
import felix.things.router.util.OpcUaUtils;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
import org.eclipse.milo.opcua.stack.core.AttributeId;
import org.eclipse.milo.opcua.stack.core.types.builtin.*;
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
import org.eclipse.milo.opcua.stack.core.types.structured.ReadResponse;
import org.eclipse.milo.opcua.stack.core.types.structured.ReadValueId;
import org.eclipse.milo.opcua.stack.core.types.structured.WriteResponse;
import org.eclipse.milo.opcua.stack.core.types.structured.WriteValue;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

import java.util.*;

@Slf4j
@Component
public class RpcProcessor implements ApplicationListener<RpcEvent> {
    private static final String RESPONSE_STATUS_OK = "ok";
    private static final String RESPONSE_FIELD_ERROR = "error";

    private Map<String, MonitorWatcher> watchers = new HashMap<>();
    private Map<String, OpcUaClient> clients = new HashMap<>();
    private Map<String, OpcUaDeviceAware> deviceContainers = new HashMap<>();

    public void putProcessor(String applicationName, MonitorWatcher watcher, OpcUaClient client, OpcUaDeviceAware deviceContainer) {
        this.watchers.put(applicationName, watcher);
        this.clients.put(applicationName, client);
        this.deviceContainers.put(applicationName, deviceContainer);
    }

    @Override
    public void onApplicationEvent(RpcEvent event) {
        if (deviceContainers.containsKey(event.getApplicationName())) {
            onRpcCommand(event.getApplicationName(), event.getDeviceName(), event.getCommand());
        } else {
            log.trace("【{}】 rpc command arrived: 【{}】", event.getApplicationName(), event);
        }
    }

    private void onRpcCommand(String applicationName, String deviceName, HashMap<String, Object> command) throws IllegalArgumentException {
        log.debug("RPC received: device='{}', command={}", deviceName, command);
        if (!deviceContainers.containsKey(applicationName)) {
            throw new IllegalArgumentException("not find RpcProcessor for [" + applicationName + "]");
        }
        OpcUaDevice device = deviceContainers.get(applicationName).getDevice(deviceName);
        if (device == null) {
            log.warn("No device '{}' found for RPC {}", deviceName, command);
            return;
        }

        Map<String, String> results = new HashMap<>();

        Map<String, NodeId> nodeIds = resolveNodeIds(device, command.keySet());
        Set<String> notFoundTags = Sets.difference(command.keySet(), nodeIds.keySet());

        if (!notFoundTags.isEmpty()) {
            log.warn("Failed to find tags {}", notFoundTags);
            results.putAll(createTagsErrorResponse(notFoundTags, "No tag found"));
        }

        Map<String, NodeId> types = requestOpcTypes(applicationName, nodeIds);
        Set<String> notResolvedTags = Sets.difference(nodeIds.keySet(), types.keySet());

        if (!notResolvedTags.isEmpty()) {
            log.warn("Failed to resolve OPC type for tags {}", notResolvedTags);
            results.putAll(createTagsErrorResponse(notResolvedTags, "Failed to resolve OPC type"));
        }

        try {
            List<WriteValue> request = createWriteRequest(nodeIds, types, command, results);

            log.trace("Writing values to OPC server for tags {}", types.keySet());

            WriteResponse writeResponse = clients.get(applicationName).write(request).get();
            results.putAll(processWriteResponse(writeResponse, types));
        } catch (Exception e) {
            log.warn("OPC write failed", e);
            results.putAll(createTagsErrorResponse(types.keySet(), "OPC write failed: " + e.getMessage()));
        }

        // RPC 结果result
        watchers.get(applicationName).onDeviceRpcResponse(applicationName, deviceName, results);
    }

    private List<WriteValue> createWriteRequest(Map<String, NodeId> tags,
                                                Map<String, NodeId> types,
                                                Map<String, Object> values,
                                                Map<String, String> results) {
        List<WriteValue> opcValues = new LinkedList<>();
        Set<String> failedTags = new HashSet<>();

        types.forEach((tag, typeNodeId) -> {
            Object rawValue = values.get(tag);
            try {
                Variant opcValue = OpcUaUtils.convertToOpcValue(typeNodeId, rawValue);
                opcValues.add(new WriteValue(tags.get(tag), AttributeId.Value.uid(), null, DataValue.valueOnly(opcValue)));
            } catch (Exception e) {
                log.warn("Failed to convert '{}' tag's value '{}' to OPC format (OPC type node {})", tag, rawValue, typeNodeId, e);
                results.put(tag, e.getMessage());
                failedTags.add(tag);
            }
        });

        if (!failedTags.isEmpty()) {
            types.keySet().removeAll(failedTags);
        }

        return opcValues;
    }

    private Map<String, String> processWriteResponse(WriteResponse response, Map<String, NodeId> tags) {
        Map<String, String> results = new HashMap<>();

        int i = 0;
        for (String tag : tags.keySet()) {
            StatusCode statusCode = response.getResults()[i++];
            if (statusCode.isGood()) {
                log.debug("OPC write success for tag '{}'", tag);
                results.put(tag, RESPONSE_STATUS_OK);
            } else {
                log.warn("OPC write failed for tag '{}': reason={}", tag, statusCode);
                results.put(tag, "OPC error code: " + statusCode);
            }
        }

        return results;
    }

    private Map<String, NodeId> requestOpcTypes(String applicationName, Map<String, NodeId> tags) {
        Map<String, NodeId> resolvedTypes = new HashMap<>();

        try {
            log.trace("Requesting OPC data type for tags {}", tags.keySet());

            List<ReadValueId> readIds = new LinkedList<>();
            tags.forEach((tag, nodeId) -> readIds.add(
                    new ReadValueId(nodeId, AttributeId.DataType.uid(), null, QualifiedName.NULL_VALUE)));

            ReadResponse readResponse = clients.get(applicationName).read(0.0, TimestampsToReturn.Neither, readIds).get();

            int readRespIndex = 0;
            for (Map.Entry<String, NodeId> entry : tags.entrySet()) {
                DataValue dv = readResponse.getResults()[readRespIndex++];

                if (dv.getStatusCode().isGood()) {
                    NodeId type = (NodeId) dv.getValue().getValue();
                    log.trace("Got OPC type node for tag '{}': {}", entry.getKey(), type);

                    resolvedTypes.put(entry.getKey(), type);
                } else {
                    log.warn("Failed to request OPC type node for tag '{}': reason={}", entry.getKey(), dv.getStatusCode());
                }
            }
        } catch (Exception e) {
            log.warn("Failed to request OPC data type for tags '{}': ", tags.keySet(), e);
        }

        return resolvedTypes;
    }

    private Map<String, NodeId> resolveNodeIds(OpcUaDevice device, Set<String> tags) {
        Map<String, NodeId> tagsToNodeId = new HashMap<>();
        for (String tag : tags) {
            NodeId tagNodeId = device.getTagNodeId(tag);
            if (tagNodeId != null) {
                tagsToNodeId.put(tag, tagNodeId);
            }
        }
        return tagsToNodeId;
    }

    private Map<String, String> createTagsErrorResponse(Set<String> tags, String error) {
        Map<String, String> errors = new HashMap<>();
        tags.forEach((tag) -> errors.put(tag, error));
        return errors;
    }
}
